package pl.edu.agh.io.fx3.treecreator.disk;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.regex.Pattern;

import pl.edu.agh.io.fx3.exceptions.ConfigFileException;
import pl.edu.agh.io.fx3.exceptions.FileAlreadyExistsException;
import pl.edu.agh.io.fx3.exceptions.FileNotFoundException;
import pl.edu.agh.io.fx3.monitor.IFile;
import pl.edu.agh.io.fx3.saver.dao.FileUtilsDAO;
import pl.edu.agh.io.fx3.treecreator.TreeCreator;
import pl.edu.agh.io.fx3.treerebuilder.DiskTreeRebuilder;
import pl.edu.agh.io.fx3.utils.ChecksumGenerator;

/**
 * Singleton class that hold logic of tree creating. It can be configured by
 * applying configuration file in File System factory.
 * 
 * @author Jaroslaw Janik
 */
public class DiskTreeCreator implements TreeCreator {

	private class KeyComparator implements Comparator<String> {
		public int compare(String s1, String s2) {
			if (s1.length() < s2.length()) {
				return -1;
			} else if (s1.length() > s2.length()) {
				return 1;
			} else {
				return s1.compareTo(s2);
			}
		}
	}

	private Map<String, DirectoryInfo> hierarchy = new TreeMap<String, DirectoryInfo>(
			new KeyComparator());
	private Map<String, DirectoryInfo> notFullDirs = new TreeMap<String, DirectoryInfo>(
			new KeyComparator());
	private DiskTreeConfig diskTreeConfig;
	private FileUtilsDAO dao;

	private static DiskTreeCreator instance = null;

	private DiskTreeCreator(File configFile, FileUtilsDAO dao)
			throws ConfigFileException {
		diskTreeConfig = setDefaultConf();

		try {
			DiskConfigFileParser cfp = new DiskConfigFileParser();
			cfp.parseConfFile(configFile, diskTreeConfig);

			this.dao = dao;
		} catch (ConfigFileException e) {
			throw e;
		}

		createHierarchy();
	}

	public File addFile(String pathname) throws IOException,
			FileAlreadyExistsException {
		if (dao.exists(pathname)) {
			throw new FileAlreadyExistsException("File " + pathname
					+ " already exists in VFS");
		}

		if (pathname.contains(File.separator)
				&& !dao.exists(pathname.substring(0,
						pathname.lastIndexOf(File.separator)))) {
			throw new IOException("Path "
					+ pathname.substring(0,
							pathname.lastIndexOf(File.separator))
					+ " does not exist");
		}

		String[] splitPath = pathname.split(Pattern.quote(File.separator));
		String name = splitPath[splitPath.length - 1];

		File newFile = null;

		for (String s : notFullDirs.keySet()) {
			DirectoryInfo di = notFullDirs.get(s);
			if (di.getFilesNumber() < diskTreeConfig.getMaxFiles()) {
				newFile = new File(s + File.separator + name);
				if (!newFile.exists()) {
					newFile.createNewFile();
					di.setFilesNumber(di.getFilesNumber() + 1);
					if (isFull(di.getFilesNumber(), di.getDirsNumber())) {
						notFullDirs.remove(s);
					}

					try {
						dao.insert(
								pathname,
								newFile.getAbsolutePath(),
								ChecksumGenerator.getChecksum(pathname,
										newFile.getAbsolutePath()));
					} catch (FileAlreadyExistsException e) {
						e.printStackTrace();
					}

					return newFile;
				}
			}
		}

		for (String s : notFullDirs.keySet()) {
			DirectoryInfo di = notFullDirs.get(s);
			if (di.getDirsNumber() < diskTreeConfig.getMaxFolders()) {
				File newDir = new File(s + File.separator + randomString());
				newDir.mkdir();
				newFile = new File(newDir.getAbsoluteFile() + File.separator
						+ name);
				newFile.createNewFile();
				di.setDirsNumber(di.getDirsNumber() + 1);
				DirectoryInfo dirInfo = new DirectoryInfo(1, 0);
				hierarchy.put(newDir.getAbsolutePath(), dirInfo);
				notFullDirs.put(newDir.getAbsolutePath(), dirInfo);

				try {
					dao.insert(
							pathname,
							newFile.getAbsolutePath(),
							ChecksumGenerator.getChecksum(pathname,
									newFile.getAbsolutePath()));
				} catch (FileAlreadyExistsException e) {
					e.printStackTrace();
				}

				return newFile;
			}
		}

		return newFile;
	}

	public boolean deleteFile(IFile file) {
		try {
			if (dao.getRealPath(file.getPath()) == "dir") {
				if (dao.listDir(file.getPath()).size() != 0)
					return false;
			}
		} catch (FileNotFoundException e) {
			return false;
		}

		File realFile = new File(file.getRealPath());

		File parent = realFile.getParentFile();

		realFile.delete();

		try {
			dao.deleteUsingVirtualPath(file.getPath());
		} catch (FileNotFoundException e) {
			return false;
		}

		parent = refactorHierarchy(parent);

		DirectoryInfo difInfo = hierarchy.get(parent.getAbsolutePath());
		difInfo.setDirsNumber(difInfo.getDirsNumber() - 1);
		notFullDirs.put(parent.getAbsolutePath(), difInfo);

		return true;
	}

	/**
	 * Returns directories which are not full.
	 * 
	 * @return List of not full directories.
	 */
	public Map<String, DirectoryInfo> getNotFullDirs() {
		return notFullDirs;
	}

	public static synchronized DiskTreeCreator getTreeCreator(File configFile,
			FileUtilsDAO dao) throws ConfigFileException {
		if (instance == null) {
			try {
				instance = new DiskTreeCreator(configFile, dao);
			} catch (ConfigFileException e) {
				throw e;
			}
		}

		return instance;
	}

	private void createHierarchy() {
		File mainDir = new File(diskTreeConfig.getRootPath()
				+ DiskTreeConfig.ROOT_DIR);

		DirectoryInfo di = new DirectoryInfo(0, 0);
		if (hierarchyExists()) {
			hierarchy.put(mainDir.getAbsolutePath(), di);
			scanHierarchy(mainDir);

			DiskTreeRebuilder diskTreeRebuilder = new DiskTreeRebuilder(dao,
					this, diskTreeConfig);
			diskTreeRebuilder.rebuildTree();
		} else {
			mainDir.mkdir();
			hierarchy.put(mainDir.getAbsolutePath(), di);
			notFullDirs.put(mainDir.getAbsolutePath(), di);
			System.out.println("Directory " + DiskTreeConfig.ROOT_DIR
					+ " not found. Create new hierarchy");
		}
	}

	private boolean hierarchyExists() {
		File mainDir = new File(diskTreeConfig.getRootPath()
				+ DiskTreeConfig.ROOT_DIR);

		return mainDir.exists();
	}

	private boolean isFull(int files, int dirs) {
		if (files == diskTreeConfig.getMaxFiles()
				&& dirs == diskTreeConfig.getMaxFolders()) {
			return true;
		}

		return false;
	}

	private String randomString() {
		UUID uuid = UUID.randomUUID();
		String myRandom = uuid.toString();

		return myRandom.substring(0, DiskTreeConfig.FOLDER_NAME_LENGTH);
	}

	private File refactorHierarchy(File file) {
		FileFilter fileFilter = new FileFilter() {
			public boolean accept(File file) {
				return file.isFile();
			}
		};

		File[] files = file.listFiles(fileFilter);
		if (files.length == 0) {
			fileFilter = new FileFilter() {
				public boolean accept(File file) {
					return file.isDirectory();
				}
			};

			files = file.listFiles(fileFilter);

			if (files.length == 0) {
				if (file.getName().compareTo(DiskTreeConfig.ROOT_DIR) != 0) {
					hierarchy.remove(file.getAbsolutePath());
					notFullDirs.remove(file.getAbsolutePath());
					File parent = file.getParentFile();
					file.delete();
					return refactorHierarchy(parent);
				}
			}
		}

		return file;
	}

	private DiskTreeConfig setDefaultConf() {
		return new DiskTreeConfig();
	}

	private void scanHierarchy(File dir) {
		FileFilter fileFilter = new FileFilter() {
			public boolean accept(File file) {
				return file.isFile();
			}
		};

		File[] files = dir.listFiles(fileFilter);

		fileFilter = new FileFilter() {
			public boolean accept(File file) {
				return file.isDirectory();
			}
		};

		File[] dirs = dir.listFiles(fileFilter);

		DirectoryInfo di = new DirectoryInfo(files.length, dirs.length);
		hierarchy.put(dir.getAbsolutePath(), di);
		if (!isFull(files.length, dirs.length)) {
			notFullDirs.put(dir.getAbsolutePath(), di);
		}

		for (File f : dirs) {
			scanHierarchy(f);
		}
	}
}
